﻿using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Graphics;

using Stopwatch = System.Diagnostics.Stopwatch;

namespace Cobalt.Content.Pipeline.Graphics.Illumination
{
	public partial class IlluminationEngine : IDisposable
	{

		private int sampleResolution = 128;
		public int SampleResolution
		{
			get { return sampleResolution; }
			set
			{
				if( value < 16 )
					value = 16;

				value = Helpers.RoundUpToPowerOf2( value );

				sampleResolution = value;
			}
		}

		private int lightmapResolution = 512;
		public int LightmapResolution
		{
			get { return lightmapResolution; }
			set
			{
				if( value < 64 )
					value = 64;

				value = Helpers.RoundUpToPowerOf2( value );

				lightmapResolution = value;
			}
		}

		private int samples = 2;
		public int Samples
		{
			get { return samples; }
			set
			{
				if( value < 1 )
					value = 1;

				samples = value;
			}
		}

		private ContentBuildLogger logger = NullBuildLogger.NullLogger;
		public ContentBuildLogger Logger
		{
			get { return logger; }
			set { logger = value ?? NullBuildLogger.NullLogger; }
		}

		public bool LogProgress { get; set; }

		public IlluminationEngine()
		{
		}

		public PixelBitmapContent<Vector3> ResolveLightmap( object lightmapKey )
		{
			if( lightmapKey == null )
				throw new ArgumentNullException();

			Lightmap lightmap;
			if( !lightmaps.TryGetValue( lightmapKey, out lightmap ) )
				return null;

			return lightmap.Resolve( DefaultLightmapLayer );
		}

		public void RenderLightmaps()
		{
			RequireMesh();
			
			InitializeGraphics();
			InitializeSurfaceGraphics();

			SetupLights();

			MapLightmaps();
			
			IlluminateLightmaps();

			Progress( "Bleeding out color values." );

			foreach( Lightmap lmap in lightmaps.Values )
				lmap.BleedEdges( samples * 3 );

			Progress( "Done!" );
		}

		private void SetupLights()
		{
			foreach( var surf in surfaces )
				surf.LinkedLights.Clear();

			foreach( LightContent light in lights )
			{
				light.Setup();

				foreach( var surf in surfaces )
				{
					if( light.LightsBox( ref surf.Bounds ) )
						surf.LinkedLights.Add( light );
				}
			}
		}

		#region MapLightmaps
		private void MapLightmaps()
		{
			Progress( "Mapping surfaces..." );

			foreach( var surf in surfaces )
			{
				Lightmap lightmap = surf.Geometry.OpaqueData.GetValue<Lightmap>( TargetLightmapKey, null );
				if( lightmap == null )
					continue;

				MapGeometryOntoLightmap( surf, lightmap );
			}

			Progress( "Done mapping surfces." );
		}

		private void MapGeometryOntoLightmap( Surface surface, Lightmap lightmap )
		{
			GeometryContent geom = surface.Geometry;

			var tcLit = (VertexChannel<Vector2>)geom.Vertices.Channels[LitCoordName];
			
			var pos = geom.Vertices.Positions;
			var norm = (VertexChannel<Vector3>)geom.Vertices.Channels[NormalName];
			var tan = (VertexChannel<Vector3>)geom.Vertices.Channels[TangentName];
			var bin = (VertexChannel<Vector3>)geom.Vertices.Channels[BinormalName];

			var tc = (VertexChannel<Vector2>)geom.Vertices.Channels[TexCoordName];

			for( int i = 0; i < geom.Indices.Count; i += 3 )
			{
				int i0 = geom.Indices[i + 0];
				int i1 = geom.Indices[i + 1];
				int i2 = geom.Indices[i + 2];

				if( i0 == i1 || i1 == i2 || i2 == i0 )
					continue;

				//winding order may be inconsistent with respect
				//to lightmap coords - we don't give a damn
				Vector2 lmc0 = tcLit[i0];
				Vector2 lmc1 = tcLit[i1];
				Vector2 lmc2 = tcLit[i2];

				Vector2 e01, e02;

				e01.X = lmc1.X - lmc0.X;
				e01.Y = lmc1.Y - lmc0.Y;
				e02.X = lmc2.X - lmc0.X;
				e02.Y = lmc2.Y - lmc0.Y;

				float area = e01.X * e02.Y - e02.X * e01.Y;

				if( area == 0 )
					//degenerate again?!
					continue;

				if( area < 0 )
				{
					Vector2 tmpCoord = lmc1;
					lmc1 = lmc2;
					lmc2 = tmpCoord;

					int tmpIdx = i1;
					i1 = i2;
					i2 = tmpIdx;
				}

				Lightmap.Vertex v0, v1, v2;

				v0.LightmapCoord = lmc0;
				v0.Position = pos[i0];
				v0.Normal = norm[i0];
				v0.Tangent = tan[i0];
				v0.Binormal = bin[i0];
				v0.TextureCoord = tc[i0];

				v1.LightmapCoord = lmc1;
				v1.Position = pos[i1];
				v1.Normal = norm[i1];
				v1.Tangent = tan[i1];
				v1.Binormal = bin[i1];
				v1.TextureCoord = tc[i1];

				v2.LightmapCoord = lmc2;
				v2.Position = pos[i2];
				v2.Normal = norm[i2];
				v2.Tangent = tan[i2];
				v2.Binormal = bin[i2];
				v2.TextureCoord = tc[i2];

				lightmap.Map( surface, ref v0, ref v1, ref v2 );
			}
		}
		#endregion

		private struct SampleTarget
		{
			public Lightmap Target;
			public int SampleIndex;
		}

		private void IlluminateLightmaps()
		{
			Progress( "Illuminating lightmaps..." );

			int iLightmap = 0;
			foreach( Lightmap lightmap in lightmaps.Values )
			{
				Progress( "Illuminating lightmap {0}...", iLightmap++ );

				Stopwatch timing = Stopwatch.StartNew();

				int sampleCount = 0;
				foreach( int sampleIndex in lightmap.GetMappedSampleIndices() )
				{
					Lightmap.SampleDesc desc;
					lightmap.GetSampleDesc( sampleIndex, out desc );

					//LightSample_BasicLight( ref desc, new SampleTarget() { Target = lightmap, SampleIndex = sampleIndex } );
					LightSample_FullIllum( ref desc, new SampleTarget() { Target = lightmap, SampleIndex = sampleIndex } );

					sampleCount++;
				}

				FinishRender();

				timing.Stop();

				Progress( "Done ({0} ms, {1} samples, {2} samples/sec)", timing.ElapsedMilliseconds,
					sampleCount, sampleCount / timing.Elapsed.TotalSeconds );
			}

			Progress( "Done" );
		}

		private void LightSample_BasicLight( ref Lightmap.SampleDesc desc, SampleTarget target )
		{
			Vector3 valueLight = new Vector3();
			foreach( LightContent light in desc.Source.LinkedLights )
			{
				if( !light.LightsPoint( ref desc.Position ) )
					continue;

				Vector3 lightDisplacement;
				Vector3.Subtract( ref light.Position, ref desc.Position, out lightDisplacement );

				float dist2 = lightDisplacement.LengthSquared();
				float dist = (float)Math.Sqrt( dist2 );

				Vector3 lightDirection;
				Vector3.Multiply( ref lightDisplacement, 1.0F / dist, out lightDirection );

				float NdL;
				Vector3.Dot( ref desc.Normal, ref lightDirection, out NdL );

				if( NdL < 0 )
					continue;

				float intensity = NdL;

				if( light is SpotLightContent )
				{
					SpotLightContent spot = (SpotLightContent)light;

					float coneDot;
					Vector3.Dot( ref spot.DirectionVector, ref lightDirection, out coneDot );
					coneDot = -coneDot;

					if( coneDot < spot.CosAngles.Y )
						//no contribution
						continue;

					if( coneDot < spot.CosAngles.X )
					{
						intensity *= (coneDot - spot.CosAngles.Y) /
							(spot.CosAngles.X - spot.CosAngles.Y);
					}
				}

				Vector3 lightContribution;
				Vector3.Multiply( ref light.LightColor,
					intensity / (light.AttenParams.X + light.AttenParams.Y * dist + light.AttenParams.Z * dist2),
					out lightContribution );

				Vector3.Add( ref valueLight, ref lightContribution, out valueLight );
			}

			target.Target.SetLuxel( DefaultLightmapLayer, target.SampleIndex, ref valueLight );
		}

		private void Progress( string text, params object[] args )
		{
			if( !LogProgress )
				return;

			Logger.LogMessage( text, args );
		}

		#region IDisposable Members

		public void Dispose()
		{
			DisposeGraphics();
		}

		#endregion
	}
}
